package com.littcore.saap.core.rest;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.jsondoc.core.annotation.ApiHeader;
import org.jsondoc.core.pojo.ApiDoc;
import org.jsondoc.core.pojo.ApiHeaderDoc;
import org.jsondoc.core.pojo.ApiMethodDoc;
import org.jsondoc.core.pojo.ApiParamDoc;
import org.jsondoc.core.pojo.ApiResponseObjectDoc;
import org.jsondoc.core.pojo.ApiVerb;
import org.jsondoc.core.scanner.AbstractJSONDocScanner;
import org.jsondoc.core.util.JSONDocType;
import org.jsondoc.core.util.JSONDocTypeBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.ValueConstants;

import com.littcore.util.StringUtils;

public class SpringJSONDocScanner extends AbstractJSONDocScanner {
  
  private static final String EMPTY = "";

  @Override
  public ApiDoc mergeApiDoc(Class<?> controller, ApiDoc apiDoc) {
    return apiDoc;
  }

  /**
   * This method merges the documentation built with the JSONDoc default scanner with data extracted from the
   * Spring's annotations.
   * Path is calculated as the concatenation between the RequestMapping "value" on the controller class
   * and the RequestMapping "value" on the method signature. Eventually replaces the "path" value of the JSONDoc annotation.
   * Verb follows the same approach as "path".
   * Produces (consumes) are calculated by looking at the RequestMapping "produces" ("consumes") and in case they are redefined
   * at method level, then those overwrite the previous ones.
   * Headers follow the same approach as "produces" and "consumes".
   * Response objects are calculated by the JSONDoc default scanner and then eventually modified in case the return object is a
   * ResponseEntity class, in that case the "responseentity" string is removed from the final documentation because not meaningful
   * for documentation users.
   * Request body is calculated by the JSONDoc default scanner. No need for integration with Spring's RequestBody annotation. 
   */
  @Override
  public ApiMethodDoc mergeApiMethodDoc(Method method, Class<?> controller, ApiMethodDoc apiMethodDoc) {
    apiMethodDoc.setVerb(getApiVerbFromSpringAnnotation(method, controller));
    apiMethodDoc.getProduces().addAll(getProducesFromSpringAnnotation(method, controller));
    apiMethodDoc.getConsumes().addAll(getConsumesFromSpringAnnotation(method, controller));
    apiMethodDoc.getHeaders().addAll(getHeadersFromSpringAnnotation(method, controller));
    apiMethodDoc.setResponse(getApiResponseObject(apiMethodDoc, method));
    apiMethodDoc.setResponsestatuscode(getResponseStatusCodeFromSpringAnnotation(apiMethodDoc, method));
    apiMethodDoc.setPath(getPathFromSpringAnnotation(apiMethodDoc, method, controller));
    apiMethodDoc.getQueryparameters().addAll(getQueryParamsFromSpringAnnotation(method, controller));
    return apiMethodDoc;
  }
  
  private String getResponseStatusCodeFromSpringAnnotation(ApiMethodDoc apiMethodDoc, Method method) {
    String responseStatusCode = apiMethodDoc.getResponsestatuscode();
    
    if(method.isAnnotationPresent(ResponseStatus.class)) {
      ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class);
      responseStatusCode = responseStatus.value().toString() + " - " + responseStatus.value().getReasonPhrase();
    }
    
    if(responseStatusCode == null) {
      responseStatusCode = HttpStatus.OK.toString();
    }
    
    return responseStatusCode;
  }

  @Override
  public ApiParamDoc mergeApiPathParamDoc(Method method, int paramIndex, ApiParamDoc apiParamDoc) {
    Annotation[] parameterAnnotations = method.getParameterAnnotations()[paramIndex];
    
    for (Annotation annotation : parameterAnnotations) {
      if(annotation instanceof PathVariable) {
        PathVariable pathVariable = (PathVariable) annotation; 
        if(!pathVariable.value().isEmpty()) {
          apiParamDoc.setName(pathVariable.value());
        }
      }
    }
    return apiParamDoc;
  }
  
  @Override
  public ApiParamDoc mergeApiQueryParamDoc(Method method, int paramIndex, ApiParamDoc apiParamDoc) {
    Annotation[] parameterAnnotations = method.getParameterAnnotations()[paramIndex];
    
    for (Annotation annotation : parameterAnnotations) {
      if(annotation instanceof RequestParam) {
        RequestParam requestParam = (RequestParam) annotation; 
        if(!requestParam.value().isEmpty()) {
          apiParamDoc.setName(requestParam.value());
        }
        apiParamDoc.setRequired(String.valueOf(requestParam.required()));
        if(!requestParam.defaultValue().equals(ValueConstants.DEFAULT_NONE)) {
          apiParamDoc.setDefaultvalue(requestParam.defaultValue());
        }
        
      }
    }
    return apiParamDoc;
  }
  
  private List<ApiParamDoc> getQueryParamsFromSpringAnnotation(Method method, Class<?> controller) {
    List<ApiParamDoc> apiParamDocs = new ArrayList<ApiParamDoc>();
    
    if(controller.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = controller.getAnnotation(RequestMapping.class);
      if(requestMapping.params().length > 0) {
        for (String param : requestMapping.params()) {
          String[] splitParam = param.split("=");
          if(splitParam != null) {
            apiParamDocs.add(new ApiParamDoc(splitParam[0], null, JSONDocTypeBuilder.build(new JSONDocType(), String.class, null), "true", new String[]{splitParam[1]}, null, null));
          } else {
            apiParamDocs.add(new ApiParamDoc(param, null, JSONDocTypeBuilder.build(new JSONDocType(), String.class, null), "true", new String[]{}, null, null));
          }
        }
      }
    }
    
    if(method.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
      if(requestMapping.params().length > 0) {
        apiParamDocs.clear();
        for (String param : requestMapping.params()) {
          String[] splitParam = param.split("=");
          if(splitParam.length > 1) {
            apiParamDocs.add(new ApiParamDoc(splitParam[0], "", JSONDocTypeBuilder.build(new JSONDocType(), String.class, null), "true", new String[]{splitParam[1]}, null, null));
          } else {
            apiParamDocs.add(new ApiParamDoc(param, "", JSONDocTypeBuilder.build(new JSONDocType(), String.class, null), "true", new String[]{}, null, null));
          }
        }
      }
    }
    
    return apiParamDocs;
  }
  
  /**
   * Gets the ApiResponseObjectDoc built by JSONDoc and checks if the first type corresponds to a ResponseEntity class. In that case removes the "responseentity"
   * string from the final list because it's not important to the documentation user.
   * @param method
   * @param apiResponseObjectDoc
   * @return
   */
  private ApiResponseObjectDoc getApiResponseObject(ApiMethodDoc apiMethodDoc, Method method) {
    if(apiMethodDoc.getResponse() != null) {
      if(method.getReturnType().isAssignableFrom(ResponseEntity.class)) {
        apiMethodDoc.getResponse().getJsondocType().getType().remove(0);
      }
    }
    
    return apiMethodDoc.getResponse();
  }
  
  /**
   * 1. Looks for headers in the RequestMapping annotation on the controller
   * 2. Looks for headers in the RequestMapping annotation on the method, overriding the ones coming from the annotation on the controller
   * 3. Looks for RequestHeader annotation on method parameters, and if ApiHeader is also present on the same parameter, adds the header to the final Set
   * @param method
   * @param controller
   * @return
   */
  private Set<ApiHeaderDoc> getHeadersFromSpringAnnotation(Method method, Class<?> controller) {
    Set<ApiHeaderDoc> headers = new LinkedHashSet<ApiHeaderDoc>();
    
    if(controller.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = controller.getAnnotation(RequestMapping.class);
      List<String> headersStringList = Arrays.asList(requestMapping.headers());
      for (String header : headersStringList) {
        String[] splitHeader = header.split("=");
        if(splitHeader.length > 1) {
          headers.add(new ApiHeaderDoc(splitHeader[0], null, new String[]{splitHeader[1]}));
        } else {
          headers.add(new ApiHeaderDoc(splitHeader[0], null, new String[]{}));
        }
      }
    }
    
    if(method.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
      if(requestMapping.headers().length > 0) {
        headers.clear();
        List<String> headersStringList = Arrays.asList(requestMapping.headers());
        for (String header : headersStringList) {
          String[] splitHeader = header.split("=");
          if(splitHeader.length > 1) {
            headers.add(new ApiHeaderDoc(splitHeader[0], null, new String[]{splitHeader[1]}));
          } else {
            headers.add(new ApiHeaderDoc(splitHeader[0], null, new String[]{}));
          }
        }
      }
    }
    
    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
    for (Annotation[] annotations : parameterAnnotations) {
      RequestHeader requestHeader = null;
      ApiHeader apiHeader = null;
      
      for (Annotation annotation : annotations) {
        if(RequestHeader.class.isAssignableFrom(annotation.annotationType())) {
          requestHeader = (RequestHeader) annotation;
        }
        if(ApiHeader.class.isAssignableFrom(annotation.annotationType())) {
          apiHeader = (ApiHeader) annotation;
        }
      }
      
      if(requestHeader != null && apiHeader != null) {
        if(!requestHeader.value().isEmpty()) {
          headers.add(new ApiHeaderDoc(requestHeader.value(), apiHeader.description(), requestHeader.defaultValue().equals(ValueConstants.DEFAULT_NONE) ? new String[]{} : new String[]{requestHeader.defaultValue()}));
        } else {
          headers.add(new ApiHeaderDoc(apiHeader.name(), apiHeader.description(), requestHeader.defaultValue().equals(ValueConstants.DEFAULT_NONE) ? new String[]{} : new String[]{requestHeader.defaultValue()}));
        }
      }
        
    }
    
    return headers;
  }
  
  /**
   * From Spring's documentation: [produces is] supported at the type level as well as at the method level! 
   * When used at the type level, all method-level mappings override this produces restriction.
   * @param method
   * @param controller
   * @return
   */
  private Set<String> getProducesFromSpringAnnotation(Method method, Class<?> controller) {
    Set<String> produces = new LinkedHashSet<String>();
    
    if(controller.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = controller.getAnnotation(RequestMapping.class);
      if(requestMapping.produces().length > 0) {
        produces.addAll(Arrays.asList(requestMapping.produces()));
      }
    }
    
    if(method.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
      if(requestMapping.produces().length > 0) {
        produces.clear();
        produces.addAll(Arrays.asList(requestMapping.produces()));
      }
    }
    
    return produces;
  }
  
  private Set<String> getConsumesFromSpringAnnotation(Method method, Class<?> controller) {
    Set<String> consumes = new LinkedHashSet<String>();
    
    if(controller.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = controller.getAnnotation(RequestMapping.class);
      if(requestMapping.consumes().length > 0) {
        consumes.addAll(Arrays.asList(requestMapping.consumes()));
      }
    }
    
    if(method.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
      if(requestMapping.consumes().length > 0) {
        consumes.clear();
        consumes.addAll(Arrays.asList(requestMapping.consumes()));
      }
    }
    
    return consumes;
  }
  
  private ApiVerb getApiVerbFromSpringAnnotation(Method method, Class<?> controller) {
    ApiVerb apiVerb = null;
    
    if(controller.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = controller.getAnnotation(RequestMapping.class);
      if(requestMapping.method().length > 0) {
        apiVerb = ApiVerb.valueOf(requestMapping.method()[0].name());
      }
    }
    
    if(method.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
      if(requestMapping.method().length > 0) {
        apiVerb = ApiVerb.valueOf(requestMapping.method()[0].name());
      }
    }
    
    if(apiVerb == null) {
      apiVerb = ApiVerb.GET;
    }
    
    return apiVerb;
  }
  
  private String getPathFromSpringAnnotation(ApiMethodDoc apiMethodDoc, Method method, Class<?> controller) {
    StringBuffer pathStringBuffer = new StringBuffer();
    
    if(controller.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = controller.getAnnotation(RequestMapping.class);
      if(requestMapping.value().length > 0) {
        pathStringBuffer.append(requestMapping.value()[0]);
      }      
    }
    else //没有定义PATH，根据package和class的名称自动生成
    {
        String canonicalName =  controller.getCanonicalName();
        //拆除domian名称和class名称
//        String[] array = canonicalName.split("[.]");
//        String className = array[array.length-1];
//        String domainName = substringBefore(canonicalName, ".rest.");
//        domainName = substringAfterLast(domainName, ".");
//        String domainName = "rest";
        String className = substringAfterLast(canonicalName, ".");
        className = substringBefore(className, "Controller");
        String domainName = "rest";
        pathStringBuffer.append("/").append(domainName).append("/").append(StringUtils.uncapitalize(className));
    }
    
    if(method.isAnnotationPresent(RequestMapping.class)) {
      RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
      if(requestMapping.value().length > 0) {
        pathStringBuffer.append(requestMapping.value()[0]);
      }
      else //没有定义PATH,解析方法名
      {
        String methodName = method.getName();
        pathStringBuffer.append("/").append(methodName);
      }
    }
    
    return pathStringBuffer.toString();
  }
  
  public static boolean isEmpty(String str) {
    return str == null || str.length() == 0;
  }
  
  public static String substringAfterLast(String str, String separator) {
    if (isEmpty(str)) {
        return str;
    }
    if (isEmpty(separator)) {
        return EMPTY;
    }
    int pos = str.lastIndexOf(separator);
    if (pos == -1 || pos == (str.length() - separator.length())) {
        return EMPTY;
    }
    return str.substring(pos + separator.length());
  }
  
  public static String substringBefore(String str, String separator) {
    if (isEmpty(str) || separator == null) {
        return str;
    }
    if (separator.length() == 0) {
        return EMPTY;
    }
    int pos = str.indexOf(separator);
    if (pos == -1) {
        return str;
    }
    return str.substring(0, pos);
}

}
